Script Object References

Up till now, we mainly learned about feeding data from C++ to our scripting environment. This tutorial will take the opposite way : how to provide data from the scripting environment to C++.

As scripting environments are often garbage collected, this will lead us to think about how we can interact with it and make the variables we extract in C++ matter in the reference counting. nkScripts offers tools to cope with that, whenever data has to be taken from an environment.

In this tutorial, we will see how we can participate in the garbage collection of an environmnent, within C++. This will be useful in many context : function parameters passing, variable extraction and reading... We will also see how script functions can be called in C++ to enable more dialog between both environments.

With these new tools we will get more expression power in safer environments. Let's dig without waiting !

Main problems

Object lifetime

As we saw in preceding tutorials, C++ can interact with the data present in an environment. Supposing we have our Data structure still around with the UserType prepared, and that we allocate one instance via :

-- Creating a data variable in the script, meaning the environment is the owner local data = nkTutorial.Data.new() ;

Now, our environment has a variable named data pointing to an instance of a Data object. We can retrieve it in C++, and keep the pointer around :

Data* dataPtr = (Data*)env->getObject("data", "nkTutorial::Data") ;

This pointer we keep around is practical and allows us to dialog easily between the environment and C++. However, at some point, our script does something along the lines of :

data = nil ;

This operation sets the only reference we have over the object to nil, making it a good candidate for garbage collection. As the environment allocated the object through a call to new, it is considered as being the memory owner, and will call the destructor assigned. As a reminder, our destructor is calling delete on the object. This means that whenever the garbage collection will run, the object will be freed, invalidating the pointer we keep around in C++. Next time we use the pointer, it's a guaranteed crash !

This is caused because C++ has not specified to the environment that it references the object too. The environment having no knowledge about that, it thought the object was good to be collected, driving us into a corner.

Calling a script function

So now we can call C++ functions right from our scripting environment, thanks to the setup of our environment. But what if we want to call a script function from C++ ? Apart from launching a script with the right sources, and retrieving right away the result of it by inspecting the right variable, there's nothing we can do.

This is sad, because the process is cumbersome. What if we overwrite a valuable variable in the process ? Why should we compile a new script just to call one tiny function ?

The answer

What we need is to be able to keep something impacting the reference counting, so that we can better control how memory is managed. We also need something allowing us to identify entities within an environment. Enters ScriptObjectReference.

Script Object Reference

The idea behind this class is that it can hold a reference over virtually any object in a scripting environment. It can be a function, an allocation... It will identify one particular object, and also participate in the reference counting of it.

Now, let's say that before putting our data variable to nil, we called :

nkScripts::ScriptObjectReference dataRef = env->getVar("Data") ;

With that call, our C++ context holds a reference over the object, also participating in the reference counting. Setting data in the scripting environment to nil is now safe : the ScriptObjectReference we have will prevent our dataPtr from being collected.

Do note that references are not specific to anything, they can hide an object reference, a function reference... For instance, we could reference a function, rather. Let's write one in our environment :

function printCall () print("Lua called !") ; end

Referencing this function in C++ could be done through :

nkScripts::ScriptObjectReference functionRef = env->getScriptFunction("printCall") ;

Now we have a reference over the function we just created. Let's request our environment to call it :

nkScripts::DataStack output ; env->callScriptFunction(functionRef, nkScripts::DataStack(), output) ;
Lua called !

If you recall the function declaration tutorials, you will probably be familiar with the concept of the DataStack. Here, we are using it to give parameters that we have to pre-fill ourselves. We also use it as an output for the function, because some scripting environments can have multiple return results. Note that the output stack needs to be pre-filled with the entries expected with the right type.

And with that, Lua got effectively called, logging the message we asked it to log ! The references can be used in many ways, as we can see. And it's not the only use nkScripts has in stock !

They can also be used automatically when specifying function callbacks. This is very useful if you have for instance a setter that can be referring to an object instanciated in the script itself. Let's augment our Data structure to have something like :

Data* _friend = nullptr ;

Now a data object can have a friend somewhere. Let's write a setter function within the environment to change it during scripting :

nkScripts::Function* setter = type->addMethod("setFriend") ; // We need a user data as the parameter to keep it as a friend setter->addParameter(nkScripts::FUNCTION_PARAMETER_TYPE::USER_DATA_PTR, "nkTutorial::Data") ; setter->setFunction ( [] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue { // Retrieve both pointers Data* data0 = (Data*)stack[0]._valUser._userData ; Data* data1 = (Data*)stack[1]._valUser._userData ; // Set the friend pointer alone data0->_friend = data1 ; return nkScripts::OutputValue::VOID ; } ) ;

Our setter will take a USER_DATA_PTR parameter, which will provide a pointer over the object linked as a parameter. As a result, because it is a method from a type, we know that the first slot will be our calling object, and the second slot will be the parameter given. Using that fact, we can just set the friend pointer in the calling object, to point to the parameter given. In action :

local data0 = nkTutorial.Data.new() ; local data1 = nkTutorial.Data.new() ; data0:setFriend(data1) ;

Everything seems perfect, however there is a problem : what if our script later sets its data1 object to nil ? The garbage collector might collect it, potentially leaving a dangling pointer within our data0 object. We need to also keep a reference over our item, and there is one parameter type dedicated to that : USER_DATA. Let's add a reference to our Data structure :

nkScripts::ScriptObjectReference _friendRef ;

And revise our setter to :

nkScripts::Function* setter = type->addMethod("setFriend") ; // We need a user data to keep as a friend, but this time with a reference setter->addParameter(nkScripts::FUNCTION_PARAMETER_TYPE::USER_DATA, "nkTutorial::Data") ; setter->setFunction ( [] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue { // Retrieve both pointers Data* data0 = (Data*)stack[0]._valUser._userData ; Data* data1 = (Data*)stack[1]._valUser._userData ; // This time also retrieve the reference created by the USER_DATA type nkScripts::ScriptObjectReference data1Ref = stack[1]._valUser._objectRef ; // Set the friend pointer alone data0->_friend = data1 ; data0->_friendRef = data1Ref ; return nkScripts::OutputValue::VOID ; } ) ;

Our parameter will be a USER_DATA this time. This type will also feed the _objectRef member of our user data parameter, which is a ScriptObjectReference. By now, you can guess that if we keep that reference, the counting will be properly done, preventing the garbage collection from happening.

This is the main difference between USER_DATA_PTR and USER_DATA. You can see the pointer variant as being a plain pointer, guarantying nothing about the lifetime. The non pointer one can be seen as a reference in the scripting sense of the word, impacting reference counting.

If you have a somewhat C++-y mind, USER_DATA might evoke you a copy. While not true in practice, the warning flag that has risen in your mind about performance holds. Distinction is important : while the pointer one is unsafe, it is also quicker, as the reference is not created. This means that like in C++ memory management, you need to think about ownership and what requires it. Nothing is free ! Sometimes the hard path has to be taken, and the reference is needed for the software to work safely. But use it wisely !

Conclusion

In this tutorial, we learned how we can manage a dialog oriented from C++ to an environment. Through ScriptObjectReference, we learned how to :

These new tools mean safest applications, as pointers can remain alive for the time you need. They also open new ways of interacting with an environment, which means more creative power !